"use strict"
/**
* The FTPClient Class constructor. 
* @param {object} socketType method of socket communication that the FTP
*     client will use; in this case it will either be MockSocket object for
*     testing purposes or the chrome socket API. 
*/
function FTPClient(socketType) {
  var BUFFER_SIZE = 1024*30;
  var NOOP_INTERVAL = 18000; /* The interval between two calls to the noop_  
  function. this is implemented to prevent disconnection from the server when 
  the client has been idle for some time. No information could be obtained
  from the server about their timeout time. However 18s seems to work in practice
  with the servers that we have tested */
  var MIN_FILE_WRITERS = 5;
  var MAX_FILE_WRITERS = 20; /*The number of writers concurrently writing to a 
  file. Once the max number of files is reached, the socket is paused until 
  there's only min file writers writing, at which point the socket is 
  unpaused. These numbers were picked based on time measurements of file 
  download*/
  // For further details on FTP Replies classification see RFC 959 Section 4.2
  var ftpReply = {
    PRELIMINARY: 1, // action initiated, expect another reply before issuing 
                   // the next command
    OK: 2, // action completed
    INTERMEDIATE: 3, // more info required to proceed further
    TEMPORARY_ERROR: 4, // requested action did not take place. Client is 
                       // encouraged to restart the command sequence
    PERMANENT_ERROR: 5 // requested action did not take place. Client is 
                      // discouraged from repeating the exact request
  };
  var NET_ERROR = {
    CONNECTION_CLOSED: -100,
    SOCKET_NOT_CONNECTED: -15
  };
  var FTPInfo = {
    socketType: socketType || chrome.sockets.tcp,
    host: "",
    commandPort: 21, // default server/peer command port
    commandId: null, // socketId for the command connection
    dataPort: null, // server/peer port used for the data connection
    dataId: null, //socketId for the data connection
    commandRawData: {
      buffer: new Uint8Array(BUFFER_SIZE),
      parsedLines: [], // received lines that constitute the server response
      buffOffset: 0  // next available slot on the buffer
    },
    lsOutput: "", // directory listing following a listDir request,
    lsDecoder: new TextDecoder("utf-8"),
    dataConnects: {}, // the current data connections
    preliminaryState: false, // received a 1yz response from the server
    preliminaryRead: null, // promise to be resolved once a server response
                          // preceded by a 1yz response has been received  
    pendingReads : [], // promises to be resolved upon received server response 
    serverResponses : [], // server responses not yet returned to the user
    noopId: null,
    writer: null,
    fileData: {
      socketId: null,
      socketPaused: false,
    }
  };

  FTPInfo.socketType.onReceive.addListener(onReceiveCallback_);
  FTPInfo.socketType.onReceiveError.addListener(onErrorCallback_);

  var enableDebug = false;

  function print () {
    if (enableDebug) {
      var message = "";
      for (var i = 0; i < arguments.length; i++) {
        message += arguments[i] + " ";
      }
      console.log(message);
    }
  }

  // defining the error classes as extention to the JavaScript Error class 
  function SendError(message) {
    this.name = "SendError";
    this.message = message || "send error";
  }

  SendError.prototype = new Error();
  SendError.prototype.constructor = SendError;

  function CreateSocketError(message) {
    this.name = "CreateSocketError";
    this.message = message || "create socket error";
  }
  
  CreateSocketError.prototype = new Error();
  CreateSocketError.prototype.constructor = CreateSocketError;

  function ConnectError(message) {
    this.name = "ConnectError";
    this.message = message || "connect error";
  }

  ConnectError.prototype = new Error();
  ConnectError.prototype.constructor = ConnectError;

  function FTPViolationError(message) {
    this.name = "FTPViolationError";
    this.message = message || "FTP protocol violation error";
  }

  FTPViolationError.prototype = new Error();
  FTPViolationError.prototype.constructor = FTPViolationError;

  function TemporaryError(message) {
    // 4xx errors
    this.name = "TemporaryError";
    this.message = message || "temporary error";
  }

  TemporaryError.prototype = new Error();
  TemporaryError.prototype.constructor = TemporaryError;

  function PermanentError(message) {
    // 5xx errors
    this.name = "PermanentError";
    this.message = message || "permanent error";
  }

  PermanentError.prototype = new Error();
  PermanentError.prototype.constructor = PermanentError;

  function UnexpectedReplyError(message) {
    // unexpected [13]xx errors
    this.name = "UnexpectedReplyError";
    this.message = message || "unexpected reply error";
  }

  UnexpectedReplyError.prototype = new Error();
  UnexpectedReplyError.prototype.constructor = UnexpectedReplyError;

  function NetworkError(message) {
    this.name = "NetworkError";
    this.message = message || "Network error";
  }

  NetworkError.prototype = new Error();
  NetworkError.prototype.constructor = NetworkError;

  function getReplyClass(reply){
    return Math.floor(getReplyCode_(reply)/100);
  }

  /**
  * The socket API for chrome offers a global socket listener which 
  * raises an event  when data has been received for a given socket. 
  * The parameter of the callback function enables the user to distinguish 
  * which socket received data and what data it received. However the 
  * global listener makes it hard to associate a reply sent by the server 
  * in response to a command. Hence a state machine was designed to associate 
  * the responses received from the server with the commands sent from the 
  * user.
  *
  * INTERNAL
  * readReply_ - adds a new promise to the queue of pending reads. If a server
  * response has already been received, the promise is resolved immediately.
  * Else the promises are resolved in the order in which they were received upon
  * getting a reply from the server.
  */
  function readReply_(command) {
    return new Promise(function (resolve, reject) {
      if (!FTPInfo.preliminaryState && FTPInfo.serverResponses.length > 0) {
        var reply = FTPInfo.serverResponses.shift();
        var result = checkReply_(reply);
        if (result instanceof Error) {
          reject(result);
        }
        else if (getReplyClass(reply) == ftpReply.PRELIMINARY) {
          FTPInfo.preliminaryState = true;
          resolve(reply);
        }
        else {
          resolve(reply);
        }
      }
      else {
        FTPInfo.pendingReads.push({"resolve": resolve, "reject": reject,
            "command": command});
      }
    });
  }
  
  /** 
  * INTERNAL
  * find_ - finds the index of the element in the data buffer.  
  * @param {Uint8Array} data The binary data where search is to be performed.
  * @param {string} elem The character that's being searched within the data
  *     buffer.
  * @return {number} Returns the index of the element within the data buffer 
  *     if found, else returns -1.
  */
  function find_(data, elem) {
    for (var i = 0; i < data.byteLength; i++) {
      if (data[i] == elem.charCodeAt(0)){
        return i;
      }
    }
    return -1;
  }

  /** 
  * INTERNAL
  * splitLines_ - given the data recieved from the server, lines are parsed  
  * and stored while the remaining raw binary data that does not form a line  
  * is stored into a buffer.
  * @param {Uint8Array} receivedData The binary data received from the server.
  */
  function splitLines_(receivedData) {
    print("entering Organize data");
    var dataStrs = [], decoder =  new TextDecoder("utf-8"),
      commandRawData = FTPInfo.commandRawData,
      prevData = commandRawData.buffer.subarray(0, commandRawData.buffOffset),
      prevString = decoder.decode(prevData, {stream:true});
    var index = find_(receivedData, "\n");
    while (index != -1) {
      // found new line
      var lineArray = receivedData.subarray(0, index+1);
      receivedData = receivedData.subarray(index+1);
      dataStrs.push(decoder.decode(lineArray));
      index = find_(receivedData, "\n");
    }
    if (dataStrs.length > 0) {
      dataStrs[0] = prevString + dataStrs[0] + decoder.decode();
      commandRawData.buffer = new Uint8Array(BUFFER_SIZE);
      commandRawData.buffOffset = 0;
    }
    commandRawData.parsedLines = commandRawData.parsedLines.concat(dataStrs);
    if (receivedData.byteLength <=
      commandRawData.buffer.byteLength - commandRawData.buffOffset) {
      // store the remianing data that does not for a line
      commandRawData.buffer.set(receivedData, commandRawData.buffOffset);
      commandRawData.buffOffset += receivedData.byteLength;
    }
    else {
      throw Error("line greater than" + BUFFER_SIZE + "characters");
    }
  }

  /** 
  * INTERNAL
  * parseResponse_ - given the parsed lines from a server response, forms and 
  * return the full single-lined or multilined response.
  * The FTP protocol specifies that the reply can be single or multi-lined. 
  * If a reply is singlelined, it's returned once parsed from the raw 
  * binary data received. If the reply is multilined the response is 
  * constructed once all its individual lines are parsed. The FTP protocol 
  * specifies that the first line of a multiline response will begin with a 
  * 3-digit code, followed immediately by a hyphen while the last line will 
  * contain the exact same 3-digit code followed immediately by a space.
  * @return {string or null} Returns the contructed server response when a 
  *     full response has been received. Else it returns null.  
  */
  function parseResponse_() {
    var commandRawData = FTPInfo.commandRawData, 
    firstLine = commandRawData.parsedLines[0], replyCode = null;
    if (commandRawData.parsedLines.length == 0) {
      return null;
    }
    if (firstLine[3] == "-" && getReplyCode_(firstLine) != null) {
      // multi-lined response
      replyCode = getReplyCode_(firstLine);
      for (var i = 1; i < commandRawData.parsedLines.length; i++) {
        var line = commandRawData.parsedLines[i];
        if (line[3] == " " && getReplyCode_(line) == replyCode) {
          // found the last line of a multiline reply
          var response = commandRawData.parsedLines.slice(0, i+1);
          commandRawData.parsedLines = commandRawData.parsedLines.slice(i+1);
          return response.join("");
        }
      }
      return null;
    }
    else {
      //single-lined response
      return commandRawData.parsedLines.shift();
    }
  }

  /**
  * INTERNAL
  * getReply_ - handles the reply received from the server. 
  * @param {Uint8Array} receivedData The binary data received from the server.
  * @return {string or null} The single or multiline constructed response or
  *    null if no full response was parsed/constructed. 
  */
  function getReply_(receivedData) {
    print("entered getReply_");
    splitLines_(receivedData);
    var commandRawData = FTPInfo.commandRawData, replies = [], response = parseResponse_();
    while (response != null) {
      replies = replies.concat(response);
      response = parseResponse_();
    }
    if (replies.length == 0) {
      return null;
    }
    return replies;
  }

  /**
  * INTERNAL
  * isLegalCode_ - checks whether a legal response code was received as part of 
  * an FTP reply. According to the FTP protocol, a response code is within the
  * 100-999 range inclusive. 
  * @param {string} replyCode A string that potentially conatins the response
  *     code sent from the server.
  */
  function isLegalCode_(replyCode) {
    var legalDigits = "0123456789";
    if (replyCode.length != 3 ||
      legalDigits.indexOf(parseInt(replyCode[0])) < 1 ||
      legalDigits.indexOf(parseInt(replyCode[1])) < 0 ||
      legalDigits.indexOf(parseInt(replyCode[2])) < 0 ) {
      return false;
    }
    return true;
  }

  /**
  * INTERNAL
  * getReplyCode_ - given a line, it checks whether the first three characters 
  * constitute a legal response code. If so, the response code is returned.
  * @param {string} replyLine A line parsed from the server response.
  * @return {number or null} Returns the response code if a valid one was 
  *     received. Else, it returns null.
  */
  function getReplyCode_(replyLine) {
    var replyCode = replyLine.slice(0,3); // 3-digit code;
    if ((replyLine[3] == " " || replyLine[3] == "-") && isLegalCode_(replyCode)) {
      return parseInt(replyCode);
    }
    return null;
  }
 
  /**
  * INTERNAL
  * readFinalReply_ - creates a promise to be resolved once the final response
  * following a preliminary one is received.  
  */
  function readFinalReply_() {
    return new Promise(function (resolve, reject) {
      if (!FTPInfo.preliminaryState) {
        reject(new Error("Final read requested in non-preliminary state"));
      }
      else {
        FTPInfo.preliminaryRead = {"resolve": resolve, "reject": reject};
        resolveReads_();
      }
    });
  }

  /**
  * INTERNAL
  * checkReply_ - given e reply received from the server, it checks whether the
  * server adheres to the FTP protocol (send a reply containing a legal 3 digit
  * code, send at most one 1yz reply per command) or whether the reply received 
  * contains an error code.
  * @param {string} reply 
  * @return {null or Error} if the reply contains a valid 3 digit code and contains no 
  * error codes ([45]xx) null is retuned, else the appropriate Error is returned.    
  */
  function checkReply_(reply) {
    var replyCode = reply.slice(0,3);
    if (!isLegalCode_(replyCode)) {
      return new FTPViolationError();
    }
    else if (FTPInfo.preliminaryState &&
      getReplyClass(reply) == ftpReply.PRELIMINARY) {
      // the server is not adhering to the FTP protocol
      // it may send at most one 1yz reply per command
      return new FTPViolationError();
    }
    else if (getReplyClass(reply) == ftpReply.TEMPORARY_ERROR) {
      return new TemporaryError(reply);
    }
    else if (getReplyClass(reply) == ftpReply.PERMANENT_ERROR) {
      return new PermanentError(reply);
    }
    else {
      // reply adheres to the FTP protocol and ir doesn't indicate any errors
      return null;
    }
  }

  /**
  * INTERNAL
  * resolveReads_ - given the received server resonses, the pending reads and 
  * the state of the system (preliminary or not), resolves the pending reads 
  * in the order in which they were recieved by returning the corresponding 
  * server response. If a preliminary response has been received no pending
  * reads are resolved until the preliminary response has been followed up
  * by a final response.  
  */
  function resolveReads_() {
    if (FTPInfo.preliminaryState && FTPInfo.serverResponses.length > 0 &&
      FTPInfo.preliminaryRead != null) {
      var reply = FTPInfo.serverResponses.shift();
      var result = checkReply_(reply);
      if (result instanceof Error) {
        FTPInfo.preliminaryRead.reject(result);
      }
      else {
        FTPInfo.preliminaryRead.resolve(reply);
      }
      FTPInfo.preliminaryState = false;
      FTPInfo.preliminaryRead = null;
    }
    while (!FTPInfo.preliminaryState && FTPInfo.pendingReads.length > 0 &&
          FTPInfo.serverResponses.length > 0) {
      var reply = FTPInfo.serverResponses.shift();
      var result = checkReply_(reply);
      if (result instanceof Error) {
        FTPInfo.pendingReads.shift().reject(result);
      }
      else if (getReplyClass(reply) == ftpReply.PRELIMINARY) {
        FTPInfo.preliminaryState = true;
        FTPInfo.pendingReads.shift().resolve(reply);
      }
      else {
        FTPInfo.pendingReads.shift().resolve(reply);
      }
    }
  }

  function setPaused (socketId, paused) {
    var socketType = FTPInfo.socketType;
    if (socketId == FTPInfo.fileData.socketId) {
      FTPInfo.fileData.socketPaused = paused;
    }
    return new Promise(function (resolve, reject) {
      function onSetPausedComplete () {
        resolve();
      }
      socketType.setPaused(socketId, paused, onSetPausedComplete);
    });
  }

  /**
  * INTERNAL
  * onReceiveCallback_ - the callback function called by the global socket 
  * listener when data is received in any of the given sockets.
  * @param {object} info Contains the socket identifier and an ArrayBuffer with
  *     the data received.
  */
  function onReceiveCallback_(info) {
    var decoder = new TextDecoder("utf-8");
    if (info.socketId == FTPInfo.commandId) {
      var receivedData = new Uint8Array(info.data);
      console.log("COMMAND CONNETION:", decoder.decode(receivedData));
      var responses = getReply_(receivedData);
      if (responses != null) {
        // got response(s)
        FTPInfo.serverResponses = FTPInfo.serverResponses.concat(responses);
      }
      resolveReads_();
    }
    else {
      for (var socketId in FTPInfo.dataConnects) {
        if (info.socketId == socketId) {
          FTPInfo.dataConnects[socketId].onReceiveData(info.data);
          break;
        }
      }
    }
  }

  function onErrorCallback_(info) {
    var socketType = FTPInfo.socketType;
    print(info.resultCode, info.resultCode == NET_ERROR.CONNECTION_CLOSED);
    if (info.resultCode == NET_ERROR.CONNECTION_CLOSED ||
        info.resultCode == NET_ERROR.SOCKET_NOT_CONNECTED) {
      if (info.socketId == FTPInfo.commandId) {
        // command connection was closed, reject all the pending reads
        for (var i = 0; i < FTPInfo.pendingReads.length; i++) {
          FTPInfo.pendingReads[i].reject(new NetworkError("Command Connection Closed"));
        }
      }
      else {
        for (var socketId in FTPInfo.dataConnects) {
          socketId = parseInt(socketId);
          if (info.socketId == socketId) {
            var connectionInfo = FTPInfo.dataConnects[socketId];
            connectionInfo.onComplete();
            delete FTPInfo.dataConnects[socketId];
            socketType.close(socketId);
            print("Called close socket");
            break;
          }
        }
      }
    }
  }

  /**
  * INTERNAL
  * noop_ - sends NOOP command to the server during idle times to keep the 
  * connection alive.
  */
  function noop_() {
    console.log("calling NOOP")
    if (FTPInfo.pendingReads.length == 0) {
      return sendCommand_("NOOP\r\n").then(function (reply) {
        if (getReplyClass(reply) != ftpReply.OK) {
          // unexpected [13]xx reply
          throw new UnexpectedReplyError();
        }
        else {
          return reply;
        }
      }).catch(function (error) {
        throw error;
      });
    }
    return null;
  }

  /**
  * INTERNAL
  * parsePWDReply_ - extracts the remote working directory. The reply sent
  * by the server could be of two forms: "<3-digit-code> "<current-dir>"" or
  * "<3-digit-code> <current-dir>".
  * @return {string or null} - the full path of the remote working directory is
  * returned if a valid response was sent by the server. Else null is returned.
  */
  function parsePWDReply_(reply) {
    var dirStart = reply.search(" "), dirEnd, dir;
    if (dirStart == -1) {
      return null;
    }
    dirStart += 1 // the char following the first space after the 3 digit code
    if (reply[dirStart] == "\"") { // dirStart points at the quotation mark
      dirEnd = reply.indexOf("\"", dirStart+1); // find end of quotation
      if (dirEnd == -1) {
        return null;
      }
      dir = reply.slice(dirStart+1, dirEnd);
    }
    else {
      dirEnd = reply.indexOf("\r", dirStart);
      if (dirEnd == -1) {
        dirEnd = reply.indexOf("\n", dirStart);
        if (dirEnd == -1) {
          return null;
        }
      }
      dir = reply.slice(dirStart, dirEnd);
    }
    if (dir == " " || dir == "") {
      return null;
    }
    if (dir[dir.length-1] != "\/") { // if dir does not end with "/", add it
      dir += "\/";
    }
    return dir;
  }

  /**
  * getCurrentDir - get the remote current working directory. The reply sent
  * by the server could be of two forms: "<3-digit-code> "<current-dir>"" or
  * "<3-digit-code> <current-dir>".
  * @return {string} - the full path of the remote working directory
  */
  function getCurrentDir() {
    return sendCommand_("PWD\r\n").then (function (reply) {
      if (getReplyClass(reply) != ftpReply.OK) {
        throw new UnexpectedReplyError();
      }
      else {
        var dir = parsePWDReply_(reply);
        if (!dir) {
          throw Error("Invalid PWD response");
        }
        else {
          return dir;
        }
      }
    }).catch(function (error) {
      throw error;
    });
  }

  /**
  * changeWorkingDir - change the remote working directory to the directory 
  * specified by the pathname.
  * @param {string} - the full path of the remote directory to switch into
  */
  function changeWorkingDir(pathname) {
    return sendCommand_("CWD " + pathname +"\r\n").then(function (reply) {
      if (getReplyClass(reply) != ftpReply.OK) {
        throw new UnexpectedReplyError();
      }
      else {
        return reply;
      }
    }).catch(function (error){
      throw error;
    });
  }

  /**
  * changeToParentDir - equivalient to the unix command cd .. 
  */
  function changeToParentDir() {
    return sendCommand_("CDUP\r\n").then(function (reply) {
      if (getReplyClass(reply) != ftpReply.OK) {
        throw new UnexpectedReplyError();
      }
      else {
        return reply;
      }
    }).catch(function (error) {
      throw error;
    });
  }

  function download(remoteEntry, localDestination, size) {
    return FileSystemUtils.constructEntryName(remoteEntry, localDestination).then(function (localVersionName) {
      return downloadFile(remoteEntry, localVersionName, localDestination, size);
    }).catch(function (error) {
      throw error;
    });
  }

  function downloadFile(originFileName, destinationFileName, destination, size) {
    var start = new Date().getTime();
    var onConnectionClosed, dataId, dataPort;
    var fileReceived = false, onDownloadComplete;
    var connectionClosed  = new Promise(function (resolve, reject) {
      var connectionClosedCallback = function () {
        resolve("Data connection closed");
      };
      onConnectionClosed = connectionClosedCallback;
    });

    var downloadCompleted = new Promise(function (resolve, reject) {
      var downloadOutcome = function (result) {
        if (result instanceof Error) {
          reject(result);
        }
        else {
          resolve(result);
        }
      };
      onDownloadComplete = downloadOutcome;
    });

    var onReceiveData = function (data) {
      var fileData =FTPInfo.fileData;
      if (!fileData.socketPaused && FTPInfo.writer.workingWriters > MAX_FILE_WRITERS) {
        fileData.socketPaused = true;
        setPaused(fileData.socketId, true);
      }
      function onComplete () {
        var fileData = FTPInfo.fileData;
        if (fileData.socketPaused && FTPInfo.writer.workingWriters < MIN_FILE_WRITERS) {
          fileData.socketPaused = false;
          setPaused(fileData.socketId, false);
        }
        if (fileReceived && FTPInfo.writer.workingWriters == 0) {
          onDownloadComplete("Download completed");
        }
      }
      FTPInfo.writer.write(data, onComplete);
    };
    var confirmDownload = dataConnect_().then(function (info) {
      dataId = info.socketId;
      dataPort = info.port;
      var connectionInfo = {
        "command": "RETR",
        "onComplete": onConnectionClosed,
        "onReceiveData": onReceiveData
      };
      FTPInfo.dataConnects[dataId] = connectionInfo;
      FTPInfo.fileData.socketId = dataId;
      return FileSystemUtils.createFile(destinationFileName, destination, size);
    }).then(function (fileEntry) {
      FTPInfo.writer = new ParallelFileWriter(fileEntry);
      return sendCommand_("RETR " + originFileName + "\r\n");
    }).then(function (reply) {
      var replyClass = getReplyClass(reply);
      if (replyClass == ftpReply.PRELIMINARY) {
        return readFinalReply_();
      }
      else if (replyClass == ftpReply.OK) {
        return reply;
      }
      else {
        // unexpected 3xx response
        throw new UnexpectedReplyError();
      }
    }).then(function (reply) {
      var replyClass = getReplyClass(reply);
      if (replyClass == ftpReply.OK) {
        return reply;
      }
      else {
        // unexpected 3xx response 
        throw new UnexpectedReplyError();
      }
    }).catch(function (error) {
      throw error;
    });
    Promise.all([confirmDownload, connectionClosed]).then(function (values) {
        // connection was closed and response was sent to acknowledge file
        // transfer completion
        console.log("BOTH PROMISES RESOLVES", values);
        var fileData = FTPInfo.fileData;
        fileReceived = true;
        var end = new Date().getTime();
        var diff = (end-start)/1000;
        FTPUI.notifyUI("time " + diff);
        if (FTPInfo.writer.workingWriters === 0) {
          // data received and file writers finished writing 
          onDownloadComplete("Download completed");
        }
      }).catch(function (error){
        onDownloadComplete(error);
      });
    return downloadCompleted;
  }

  /**
  * listDir - returns the promise of a directory listing once one is received.
  * The function returns a directory listing once the data connection has been
  * closed by the server indicating EOF and once a confirmation reply has been
  * received through the command connection.
  * @param {string} pathname
  * @return {array or Error} returns an array containing the confirmation reply 
  *     and the directory listing if the action suceeds, else returns the value 
  *     of the promise that rejected.
  */
  function listDir(pathname) {
    print("entering listDir");
    var listCommand, onListDirComplete, dataId, dataPort;
    if (pathname == undefined) {
      // list current directory
      listCommand = "LIST\r\n";
    }
    else {
      listCommand = "LIST " + pathname + "\r\n";
    }
    // the promise of returning a directory listing if one is received
    var directoryList = new Promise(function (resolve, reject) {
      // resolved once data connection is closed
      var listDirCallback = function () {
        FTPInfo.lsOutput = FTPInfo.lsOutput + FTPInfo.lsDecoder.decode();
        resolve(FTPInfo.lsOutput);
        FTPInfo.lsOutput = "";
      };
      onListDirComplete = listDirCallback;
    });
    // register the callback function for received data
    var onReceiveData = function (data) {
      data = new Uint8Array(data);
      FTPInfo.lsOutput += FTPInfo.lsDecoder.decode(data, {stream:true});
    };
    // establish data connection and request the directory listing
    var confirmDirReceit = dataConnect_().then(function (info) {
      dataId = info.socketId;
      dataPort = info.port;
      var connectionInfo = {
        "command": "LIST",
        "onComplete": onListDirComplete,
        "onReceiveData": onReceiveData
      };
      FTPInfo.dataConnects[dataId] = connectionInfo;
      print("IN list dir, dataConnect_ suceeded");
      return sendCommand_(listCommand);
    }).then(function (reply) {
      var replyClass = getReplyClass(reply);
      if (replyClass == ftpReply.PRELIMINARY) {
        print("List dir received prelim response");
        return readFinalReply_();
      }
      else if (replyClass == ftpReply.OK){
        return reply;
      }
      else {
        // 3xx response not expected
        throw new UnexpectedReplyError();
      }
    }).then(function (reply) {
      var replyClass = getReplyClass(reply);
      if (replyClass == ftpReply.OK) {
        return reply;
      }
      else {
        // 3xx response not expected
        throw new UnexpectedReplyError();
      }
    }).catch(function (error) {
      throw error;
    });
    return Promise.all([confirmDirReceit, directoryList]);
  }

  /**
  * INTERNAL
  * createSocket_ - a wrapper function around the chrome.sockets.tcp.create 
  * function to mimick a synchronous behaviour using javascript native promises.
  */
  function createSocket_() {
    var socketType = FTPInfo.socketType;
    return new Promise(function (resolve, reject) {
      function onCreateComplete (socketInfo) {
        print("onCreateComplete", socketInfo.socketId);
        if (socketInfo.socketId >= 0) {
          resolve(socketInfo);
        }
        else {
          reject(new CreateSocketError());
        }
      };
      socketType.create({bufferSize: BUFFER_SIZE}, onCreateComplete);
    });
  }
  
  /**
  * INTERNAL
  * connectWrapper_ - a wrapper function around the chrome.sockets.tcp.connect 
  * function to mimick a synchronous behaviour using javascript native promises.
  */
  function connectWrapper_(socketId, host, port) {
    print("connect wraper socketId, host, port:", socketId, host, port);
    var socketType = FTPInfo.socketType;
    return new Promise(function (resolve, reject) {
      function onConnectionComplete (result) {
        console.log("ConnectWrapper_ result = " + result);
        if (result < 0) {
          reject(new ConnectError("Error code " + result));
        }
        else {
          resolve(result);
        }
      };
      socketType.connect(socketId, host, port, onConnectionComplete);
      print("called socket.tcp.connect")
    });
  }

  /**
  * connect - A public method that creates a socket and connects to a host 
  * on the given port, which defaults to 21.
  * @param {string} host The address of the remote machine. Chrome API support 
  *     DNS name, IPv4 abd IPv6 formats.
  * @param {number} port The port of the remote machine. 
  */
  function connect(host, port) {
    if (port != undefined && port != "") {
      FTPInfo.commandPort = parseInt(port);
    }
    FTPInfo.host = host;
    return createSocket_().then(function (socketInfo) {
      console.log("Created socket with socketId:", socketInfo.socketId);
      print("Now connecting...");
      FTPInfo.commandId = socketInfo.socketId;
      print("set the commandId", FTPInfo.commandId);
      return connectWrapper_(FTPInfo.commandId, FTPInfo.host,
              FTPInfo.commandPort);
    }).then(function () {
      return readReply_("welcomeMsg");

    }).then(function (connectMsg) {
      var replyClass = getReplyClass(connectMsg);
      if (replyClass != ftpReply.OK) {
        // unexpected [13]xx reply code 
        throw new UnexpectedReplyError(connectMsg);
      }
      else {
        var timerId = setInterval(noop_, NOOP_INTERVAL); /* assumed 18 s would be an  
                                                /* appropriate value? */
        FTPInfo.noopId = timerId;
        return connectMsg;
      }
    }).catch(function (error) {
      print("error in connect", error);
      throw error;
    });
  }

  function parsePASVReply_(reply) {
    print("entering parsePASV", reply);
    var re = /\d{1,3},\d{1,3},\d{1,3},\d{1,3},(\d{1,3}),(\d{1,3})/;
    var hostPort = re.exec(reply);
    if (hostPort == null) {
      return null;
    }
    else if (parseInt(hostPort[1]) > 256 || parseInt(hostPort[2]) > 256) {
      return null;
    }
    else {
      return parseInt(hostPort[1])<<8 | parseInt(hostPort[2]);
    }
  }

  /**
  * INTERNAL
  * parseEPSVReply_ - extracts the port number that the peer/server will use 
  * for the data connection. The response to an EPSV command must be 
  * <some text> (<d><d><d><tcp-port><d>), where <d> is a delimiter character. 
  * @param {string} reply The reply received from the server in reponse to the
  *    EPSV command.
  * @return {number} The port the peer/server will use for the data connection.
  */
  function parseEPSVReply_(reply) {
    var portStr = reply.match(/\(\S{3}(\d{5})\S\)/)[1];
    if (portStr === null) {
      return null;
    }
    return parseInt(portStr);
  }

  /**
  * INTERNAL
  * dataConnect_ - establishes the data connection. The port number that the 
  * server will use is extracted from the reply to the PASV command.
  */
  function dataConnect_() {
    var socketType = FTPInfo.socketType, dataPort, dataId;
    return sendCommand_("EPSV\r\n").then(function (reply) {
      var replyClass = getReplyClass(reply);
      if (replyClass != ftpReply.OK) {
        // unexpected [13]xx reply
        throw UnexpectedReplyError();
      }
      else {
          dataPort = parseEPSVReply_(reply);
          if (!dataPort) {
            throw new Error("Invalid EPSV reply: " + reply);
          }
          else {
            return createSocket_();
          }
      }
    }).then(function (socketInfo) {
      dataId = socketInfo.socketId;
      return connectWrapper_(dataId, FTPInfo.host,
              dataPort);
    }).then(function (result) {
      return {"socketId": dataId, "port": dataPort};
    }).catch(function (error) {
      console.log("Error in dataConnect_", error);
      throw error;
    });
  }

  /**
  * closeConnections - close the command and data connections and remove the onReceive 
  * and onError listener.
  */
  function closeConnections() {
    var socketType = FTPInfo.socketType;
    socketType.onReceive.removeListener(onReceiveCallback_);
    socketType.onReceiveError.removeListener(onErrorCallback_);
    clearInterval(FTPInfo.noopId);
    socketType.close(FTPInfo.commandId);
    for (var socketId in FTPInfo.dataConnects) {
      // close any data connection still left open
      socketId = parseInt(socketId);
      delete FTPInfo.dataConnects[socketId];
      socketType.disconnect(socketId);
      socketType.close(socketId);
    }
  }

  function logOut() {
    var socketType = FTPInfo.socketType;
    sendCommand_("QUIT\r\n").then(function (reply) {
      closeConnections();
    }).catch(function (error) {
      // QUIT command was not recognized. Close the existent connections 
      // anyway. This case should not be reached unless the server is not 
      // adhering to the FTP protocol.
      closeConnections();
    });
  }
    
  /**
  * INTERNAL
  * sendCommand_ - a wrapper function around the chrome.sockets.tcp.send function 
  * to mimick a synchronous behaviour using javascript native promises.
  * @param {string} command Begins with an FTP command followed by an argument 
  *     field (if any) and ends with CRLF.
  * @return {promise} The promise of a reply received from the server 
  *     uppon sending an FTP command.
  */
  function sendCommand_(command) {
    var encoder = new TextEncoder("utf-8"), socketType = FTPInfo.socketType;
    var commandBuff = encoder.encode(command).buffer; // get ArrayBuffer
    var promise = new Promise(function (resolve, reject) {
      function onCompleteSend (sendResult) {
        if (sendResult.resultCode < 0) {
          reject(new SendError("Error code " + sendResult.resultCode));
        }
        else {
          resolve(sendResult);
        }
      }
      socketType.send(FTPInfo.commandId, commandBuff, onCompleteSend);
    });
    return promise.then(function () {
      return readReply_(command);
    }).catch(function (error) {
      throw error;
    });
  }

  /*
  * login - Log in as the given user. If no user specified, it default to 
  * anonymous. Anonymous FTP is a means by which archive sites allow general
  * access to their information. User "ananymous" has limited access to the 
  * host (logging in, listing contents of a limited set of directories and 
  * retrieving files). If user is 'anonymous', the password default is 
  * 'anonymous@'. Traditionally, the 'anonymous' user account accepts any string
  * as a password (RFC 1635).
  * @param {string} username
  * @param {string} password
  * @return {string} reply sent from the server upon the attempt to login as 
  *     the given user.
  */
  function login(username, password) {
    var encoder = new TextEncoder("utf-8");
    print("LOGIN");
    print(FTPInfo.host);
    if (username == "" || username == undefined) {
      username = "anonymous";
    }
    if (password == undefined) {
      password = "";
    }
    if (username == "anonymous") {
      password = "anonymous@";
    }
    return sendCommand_("USER " + username + "\r\n").then(function (reply) {
      print("Sent user info. Now getting Response");
      var replyClass = getReplyClass(reply);
      if (replyClass == ftpReply.INTERMEDIATE) {
          // username OK, need password
          print("userName OK");
          return sendCommand_("PASS " + password + "\r\n");
      }
      else if (replyClass == ftpReply.OK) {
          // server does not ask for password: 230 Logged in, proceed!
          return reply;
      }
      else {
        // unexpected 1xx reply
        throw new UnexpectedReplyError();
      }
    }).then(function (reply) {
      // logged in, proceed
      print("Sent password. Getting reply");
      var replyClass = getReplyClass(reply);
      if (replyClass == ftpReply.OK) {
          print("Login successful");
          return reply;
      }
      else if (replyClass == ftpReply.INTERMEDIATE) {
          // need ACCT information. Need to add an input box to the 
          // initial window to ask for this information 
          print("Need ACCT information");
      }
      else {
        // unexpected 1xx reply
        throw new UnexpectedReplyError();
      }
    }).catch(function (error) {
      print("ERROR", error);
      throw error;
    });
  }

  this.login = login;
  this.connect = connect;
  this.getCurrentDir = getCurrentDir;
  this.changeWorkingDir = changeWorkingDir;
  this.listDir = listDir;
  this.changeToParentDir = changeToParentDir;
  this.download = download;
  this.logOut = logOut;
  this.test = {
    FTPInfo: FTPInfo,
    readReply_: readReply_,
    splitLines_: splitLines_,
    parseResponse_: parseResponse_,
    parsePASVReply_ : parsePASVReply_,
    parseEPSVReply_ : parseEPSVReply_,
    getReply_: getReply_,
    readFinalReply_: readFinalReply_,
    isLegalCode_: isLegalCode_,
    getReplyCode_: getReplyCode_,
    onReceiveCallback_: onReceiveCallback_,
    createSocket_: createSocket_,
    connectWrapper_: connectWrapper_,
    sendCommand_: sendCommand_,
    dataConnect_: dataConnect_,
    noop_: noop_,
    parsePWDReply_: parsePWDReply_
  };
}



